Render strategies are per-field UI modifiers that change how a datatype is displayed, for example how a binding's editor is displayed in the recipe editor. They don't change a field's value or type — only how it looks and behaves on screen.
Every datatype carries an optional renderStrategies array. Each strategy is an object with a type (the strategy name) and a config (strategy-specific options):
{
"type": "S",
"name": "First Name",
"renderStrategies": [
{
"type": "addClasses",
"config": { "classes": ["w-33"] }
}
]
}
Strategies cover three broad concerns: how the input control looks (e.g. valueAsSlider, valueAsTextArea, passwordMasking), how a container of fields is laid out (e.g. templatedEditor, templatedLayout, inlineLayout), and styling/visibility on the field's DOM (e.g. addClasses, hidden, important). A field can list more than one strategy — they're applied in order.
Strategies that control how a container of fields (usually O or A types, which represent strongly typed objects (where each field and its value type are defined), and strongly typed homogenous arrays respectively) is laid out.
Custom editable HTML layout for an O type. Child editors are injected into elements matched by CSS class, preserving full edit and validation behaviour. See the two-column form worked example below for a full demonstration. In this example, the HTML with class hooks represents HTML where elements within the HTML who have classes which exactly match a field name (case sensitive) have their contents replaced with the editor for that field.
{ "type": "templatedEditor", "config": { "template": "<HTML with class hooks>" } }
Read-only sibling of templatedEditor — same class-name injection mechanic but renders read-only. Use when the binding is manual: false (computed) and you want a custom display.
{ "type": "templatedLayout", "config": { "template": "<HTML with class hooks>" } }
Current value:
{}
Custom HTML for an A type. Provides separate templates for the array container and for each item, with CSS selectors telling the editor where to inject items and where the child editor sits inside an item.
{
"type": "templatedArrayLayout",
"config": {
"template": "<div class='itemList'></div>",
"itemTemplate": "<div class='item'><span class='value'></span></div>",
"itemTemplateValueSelector": ".value",
"childrenContainerSelector": ".itemList"
}
}
Current value:
{}
Read-only HTML template with {{ fieldName }} token substitution. Lighter than templatedLayout when you just want to interpolate values into a string.
{ "type": "replacementLayout", "config": { "template": "Hello {{ name }}!" } }
Current value:
{}
Renders the field (and, for O/A types, its children) inline rather than as block-level. Useful for compact horizontal layouts.
{ "type": "inlineLayout", "config": {} }
Current value:
{}
Applies a list of strategies to a single named child of an O-type binding, rather than to the binding itself. Useful when the strategies you want belong on a sub-field but you want to keep them grouped with the parent's definition.
{
"type": "child",
"config": { "field": "email", "strategies": [ { "type": "placeholder", "config": {} } ] }
}
Current value:
{}
Strategies that replace the default editor for a type with a different control.
Renders a numeric field (I, L, F, D) as an HTML range slider with optional tick marks.
{ "type": "valueAsSlider", "config": { "min": 0, "max": 100, "tickEvery": 10, "displayTicks": true } }
Current value:
{}
Read-only numeric display rendered as a gauge / progress bar between min and max.
{ "type": "valueAsGauge", "config": { "min": 0, "max": 100 } }
Current value:
{}
String field rendered as a CodeMirror editor — the same JS editor used for Js fields. Useful when the string is code or another structured language.
{ "type": "valueAsCodeMirror", "config": {} }
Current value:
{}
String field rendered as a multi-line textarea instead of a single-line input.
{ "type": "valueAsTextArea", "config": {} }
Current value:
{}
String field rendered as a rich text (WYSIWYG) editor. Output is HTML.
{ "type": "valueAsRichText", "config": {} }
Boolean or choice field rendered as radio buttons rather than a checkbox or dropdown.
{ "type": "valueAsRadioButton", "config": {} }
Current value:
{}
Boolean field rendered as a select / dropdown rather than a checkbox.
{ "type": "valueAsDropdown", "config": {} }
Current value:
{}
Timestamp / numeric field rendered as a date picker (date portion only).
{ "type": "valueAsDatePicker", "config": {} }
Current value:
{}
Timestamp / numeric field rendered as a datetime picker (date + time of day).
{ "type": "valueAsDateTimePicker", "config": {} }
Current value:
{}
Read-only inline display of the field's value. Combines read-only with an inline (non-block) layout.
{ "type": "inlineReadOnly", "config": {} }
Current value:
{}
Boolean rendered as a checkmark / cross icon rather than a checkbox.
{ "type": "simpleBoolean", "config": { "size": 24 } }
Current value:
{}
Renders only the field's value, with no surrounding label or chrome. The Simple strategy is an alias for valueOnlyLayout.
{ "type": "valueOnlyLayout", "config": {} }
Current value:
{}
Strategies that decorate the field's DOM — CSS classes, placeholder text, prefixes / suffixes, or hide/highlight states.
Marks the field visually as required / important (adds the .required class to the field's label).
{ "type": "important", "config": {} }
Current value:
{}
Promotes the field's default value to the input's HTML placeholder attribute, so the user sees it as ghost text rather than a pre-filled value.
{ "type": "S", "name": "Email", "default": "you@example.com", "renderStrategies": [{ "type": "placeholder", "config": {} }] }
Current value:
{}
Hides the field from the UI. The value is still part of the binding's data, just not visible or editable. Useful for fields populated by upstream auto-bindings.
{ "type": "hidden", "config": {} }
Inserts the field's configured prefix HTML before the input control. The prefix text comes from a sibling prefix property on the datatype.
{ "type": "S", "name": "Path", "prefix": "/", "renderStrategies": [{ "type": "prefix", "config": {} }] }
Current value:
{}
Inserts the field's configured suffix HTML after the input control.
{ "type": "S", "name": "Domain", "suffix": ".com", "renderStrategies": [{ "type": "suffix", "config": {} }] }
Current value:
{}
Renders a string field as a password input (characters masked).
{ "type": "passwordMasking", "config": {} }
Current value:
{}
Like passwordMasking, but with an eye / reveal toggle so the user can see the value they're typing.
{ "type": "revealablePasswordMasking", "config": {} }
Current value:
{}
Wraps the field's hint text in a collapsible container, hiding long descriptions until the user expands them.
{ "type": "bindingHint", "config": {} }
Current value:
{}
Suppresses the validation UI for this field. Validation rules still run, but failures don't show inline errors. Use sparingly.
{ "type": "noValidation", "config": {} }
Adds CSS classes to the field's root element. Often used to set widths (w-33, w-66) when many fields share a row.
{ "type": "addClasses", "config": { "classes": ["w-33", "text-bold"] } }
Adds CSS classes to one or more descendants of the field's root, selected by CSS selector. Use when you need to style the inner input, label, or other sub-elements specifically.
{
"type": "addClassesTo",
"config": { "targets": [ { "selector": ".configurableArgumentName", "classes": ["text-uppercase"] } ] }
}
Inverse of addClasses — removes CSS classes from the field's root element.
{ "type": "removeClasses", "config": { "classes": ["mb-2"] } }
Inverse of addClassesTo — removes CSS classes from descendants matched by selector.
{
"type": "removeClassesFrom",
"config": { "targets": [ { "selector": ".configurableArgumentName", "classes": ["text-bold"] } ] }
}
Strategies that change how a value is displayed — conversions and presentations rather than edit controls.
Formats a numeric (epoch ms) field as a human-readable date / time.
{ "type": "numericAsTime", "config": {} }
Current value:
{}
Formats a numeric duration as a human-readable timespan (e.g. 2h 15m). The timeUnit tells the editor what unit the underlying number is in.
{ "type": "valueAsTimeSpan", "config": { "timeUnit": "ms", "precision": 2 } }
Current value:
{}
Read-only display of a numeric value (I, L, D, F), coloured according to numeric threshold-keyed colour mappings. The colour applied is the one whose threshold key is the largest the value meets or exceeds.
{ "type": "colorCodedReadOnly", "config": { "colors": { "0": "#f00", "30": "#fa0", "70": "#0c0" } } }
Current value:
{}
Numeric variant of colorCodedReadOnly — thresholds are numeric and the value's colour follows the threshold band it falls into.
{ "type": "colorCodedNumeric", "config": { "colors": { "0": "#0f0", "100": "#f00" } } }
Current value:
{}
Replaces a substring inside an HTML template with the field's value. Lighter than replacementLayout — single-token substitution rather than a full template.
{ "type": "htmlSubstitution", "config": { "baseString": "Score: __SCORE__", "replacementString": "__SCORE__" } }
Strategies that change how a container field (usually A or O) manages its children — collapse state, length limits, or layout direction.
Wraps the field's content in a collapsible container with configurable collapsed and expanded labels. Good for O types with many fields you don't want to show at all times.
{ "type": "collapsible", "config": { "isCollapsed": true, "collapsedContent": "Show advanced", "expandedContent": "Hide advanced" } }
Current value:
{}
Enforces minimum and / or maximum size on an A (array) type. The editor shows add / remove buttons only when constraints allow.
{ "type": "lengthConstrainedA", "config": { "minSize": 1, "maxSize": 5 } }
Current value:
{}
Lays out an A type's items horizontally rather than the default vertical stack.
{ "type": "horizontallyStackedA", "config": {} }
Current value:
{}
Combination of horizontallyStackedA and lengthConstrainedA — horizontal layout with min / max constraints.
{ "type": "horizontallyStackedLengthConstrainedA", "config": { "minSize": 1, "maxSize": 3 } }
Current value:
{}
These strategies are what runs when no renderStrategies are supplied for a given type. You rarely set them explicitly — they're the baseline editors. Listed here for completeness.
SDefault — default string editor (single-line input).NumericDefault — default editor for numeric types (I, L, F, D).BDefault — default boolean editor (checkbox).ODefault — default object editor (labelled fields stacked vertically).ADefault — default array editor (vertical list with add / remove).ChoiceDefault — default editor for CHOICE types (variant selector + nested editor).JSDefault — default JavaScript editor (CodeMirror).JvDefault — default editor for Jv (raw JSON).DscDefault — default editor for Dsc (description) types — renders text or HTML.BYTSDefault — default editor for BYTS (byte array) — file upload control.EDefault — default editor for E (exception) types — read-only structured error display.LazyDefault — defers editor selection to a JS function that computes the effective type at render time.Suppose a recipe needs a binding called Attribute Mapping that asks the user to fill in four named attributes (first name, last name, email, role) and produces an object the rest of the recipe can consume. The default rendering of an O-type binding stacks each field vertically with its prompt above the input. Useful but verbose — for four short text fields we'd rather show labels on the left and inputs on the right.
That's what the templatedEditor strategy is for. It accepts an HTML template; child editors are injected into elements whose CSS class matches the child's name. Anything else in the template (labels, layout containers, dividers) is left alone.
{
"type": "O",
"name": "Attribute Mapping",
"renderStrategies": [
{
"type": "templatedEditor",
"config": {
"template": "<div style='display:grid; grid-template-columns: 1fr 2fr; gap: 8px 16px; align-items: center;'><label>First Name</label><div class='firstName'></div><label>Last Name</label><div class='lastName'></div><label>Email</label><div class='email'></div><label>Role</label><div class='role'></div></div>"
}
}
],
"fields": [
{ "type": "S", "name": "firstName" },
{ "type": "S", "name": "lastName" },
{ "type": "S", "name": "email" },
{ "type": "S", "name": "role" }
]
}
Reading the template with the HTML un-escaped:
<div style="display:grid; grid-template-columns: 1fr 2fr; gap: 8px 16px; align-items: center;">
<label>First Name</label> <div class="firstName"></div>
<label>Last Name</label> <div class="lastName"></div>
<label>Email</label> <div class="email"></div>
<label>Role</label> <div class="role"></div>
</div>
Each <div class="..."> is an injection site. When the editor renders, the firstName child editor is placed inside <div class="firstName">, the lastName child inside <div class="lastName">, and so on. CSS Grid arranges them as a two-column form.
The exact binding above, rendered live below. Edit the fields and watch the value update.
Current value:
{}
The binding's value is the same shape you'd get from a plain O binding, keyed by the child field names. If you want the output keyed by the human-readable label rather than a code-friendly identifier, name the field that way directly: "name": "First Name" is a valid field name.
<div class="firstname"> won't match a field named firstName — the match is case-sensitive.template can be a JavaScript function function(dataType, value) { ... } that returns the HTML. Useful when the layout depends on the current value.